/*
 * Copyright (C) 2010 Eric Bottard
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.excilys.oss.dao.dynamicfinders;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Base class for providing implementations of DAOs from an interface alone.
 * Subclasses should implement {@link #buildInvocationHandler(Class, Map)} and
 * may use ORM specific concepts (such as an Hibernate SessionFactory) if they
 * wish. Implementations should be thread safe, typically stateless.
 * 
 * @author Eric Bottard
 * 
 */
public abstract class DynamicFinderFactory {

	private static final String PROPERTY = "[A-Z][a-zA-Z]*?";

	private static final String BOOL_OP = "And|Or";

	private static final String ALL_COMPARATORS;
	static {
		StringBuilder sb = new StringBuilder();
		for (Comparator c : Comparator.values()) {
			sb.append(c.toString()).append('|');
		}
		ALL_COMPARATORS = sb.substring(0, sb.length() - 1);
	}

	private static final String ONE_PROP_FINDER = "(" + PROPERTY + ")("
			+ ALL_COMPARATORS + ")?";

	private static final Pattern METHOD_PATTERN = Pattern
			.compile("^find(All)?By" + ONE_PROP_FINDER + "((" + BOOL_OP + ")"
					+ ONE_PROP_FINDER + ")?$");

	public static void main(String[] args) {
		System.out.println(METHOD_PATTERN);
	}

	public <T> T buildDAO(Class<T> itf) {

		Map<Method, FinderDescriptor> descriptors = buildFinderDescriptors(itf);
		InvocationHandler h = buildInvocationHandler(itf, descriptors);

		return (T) Proxy.newProxyInstance(Thread.currentThread()
				.getContextClassLoader(), new Class[] { itf }, h);
	}

	protected abstract <T> InvocationHandler buildInvocationHandler(
			Class<T> itf, Map<Method, FinderDescriptor> descriptors);

	private <T> Map<Method, FinderDescriptor> buildFinderDescriptors(
			Class<T> itf) {
		Method[] methods = itf.getMethods();

		Map<Method, FinderDescriptor> descriptors = new HashMap<Method, FinderDescriptor>(
				methods.length);

		for (Method m : methods) {
			Matcher matcher = METHOD_PATTERN.matcher(m.getName());
			if (!matcher.matches()) {
				throw new IllegalArgumentException("Unable to parse " + m
						+ " as a finder method");
			}
			String all = matcher.group(1);
			if (all == null
					&& Collection.class.isAssignableFrom(m.getReturnType())) {
				throw new IllegalArgumentException(
						"Incompatible return type for " + m
								+ ". Expected a single result.");
			} else if (all != null
					&& !List.class.isAssignableFrom(m.getReturnType())) {
				throw new IllegalArgumentException(
						"Incompatible return type for " + m
								+ ". Expected a java.util.List");
			}

			int neededArgs = 0;

			String propertyName1 = matcher.group(2);
			propertyName1 = propertyName1.substring(0, 1).toLowerCase()
					+ propertyName1.substring(1);

			String comparatorString1 = matcher.group(3);
			Comparator comp1 = (comparatorString1 == null) ? Comparator.Equals
					: Comparator.valueOf(comparatorString1);

			neededArgs += comp1.nbArguments;

			String boolComb = null;
			String propertyName2 = null;
			Comparator comp2 = null;

			if (null != matcher.group(4)) {
				boolComb = matcher.group(5);
				propertyName2 = matcher.group(6);
				propertyName2 = propertyName2.substring(0, 1).toLowerCase()
						+ propertyName2.substring(1);
				String comparatorString2 = matcher.group(7);
				comp2 = (comparatorString2 == null) ? Comparator.Equals
						: Comparator.valueOf(comparatorString2);
				neededArgs += comp2.nbArguments;
			}

			if (neededArgs != m.getParameterTypes().length) {
				throw new IllegalArgumentException(m
						+ " has the wrong number of arguments. " + neededArgs
						+ " expected.");
			}

			BooleanCombinator combinator = boolComb != null ? BooleanCombinator
					.valueOf(boolComb) : null;
			descriptors.put(m, new FinderDescriptor(all != null, combinator,
					propertyName1, comp1, propertyName2, comp2));

		}

		if (descriptors.size() == 0) {
			throw new IllegalArgumentException("Type " + itf
					+ " has no finder methods. At least one finder is expected");
		}

		return descriptors;
	}
}
